package pl.ps.wi.monitoring.station.services;

import java.io.File;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.List;
import java.util.LinkedList;
import java.util.Timer;
import java.util.TimerTask;
import java.util.Date;

import pl.ps.wi.monitoring.station.LocalConfiuration;
import pl.ps.wi.monitoring.station.Phone;
import pl.ps.wi.monitoring.station.interfaces.VisitorsLimitExceeded;

/**
 * @file VisitorCount.java
 * @class VisitorCount
 * @date 12.06.2010
 * @version 1.0
 *
 * @brief Klasa służy do zliczenia osób widocznych na obrazie z kamery
 *
 */
public class VisitorCount {

/**
 * Numer urządzenia
 */
	private int device;

/**
 * Liczba przecięć prostokątów w klasyfikatorze Haara, niezbędnych aby uznać obszar za prawidłowo wykryty obiekt.
 */
	private int rectangles;

/**
 * Plik klasyfikatora Haara
 */
	private String filename;

/**
 * Katalog roboczy programu
 */
	private String directory;

/**
 * Ile razy dany wynik musi się powtórzyć w sekwencji ostatnich klatek, aby uznać go za wiarygodny
 * (w trakcie działania występuje wiele krótkotrwałych zakłóceń, mogących zniekształcić wynik)
 */
	private int threshold;

/**
 * Minimalny rozmiar zlokalizowanego obszaru
 */
	private int size;

/**
 * Liczba zapamiętywanych wyników w programie visitorcount
 */
	private int history;
	
/**
 * Zmienna służąca do synchronizacji (uniemożliwia kilkukrotne uruchomienie programu)
 */
	private static final Object lock = new Object();

/**
 * Lista obiektów oczekująca zdarzenia VisitorsLimitExceeded
 */
	private List<VisitorsLimitExceeded> observers = new LinkedList<VisitorsLimitExceeded>();

/**
 * Zmienna logiczna określająca czy wątek działa, zmiany wartości można dokonać poprzez metody watch(), stop()
 */
	private boolean run = false;

/**
 * Maksymalna liczba osób jaka może być w pomieszczeniu
 */
	private int visitorsLimit = Integer.MAX_VALUE;

/**
 * Czas monitorowania, pobierany z pliku konfiguracyjnego aplikacji
 */
	private int time;

/**
 * Konstruktor klasy.
 * @param conf Obiekt konfiguracyjny, zawierający opcje wywołania programu zliczania osób.
 */
	public VisitorCount(LocalConfiuration conf) {
		this.device = conf.getVCDevice();
		this.filename = conf.getVCDetector();
		this.directory = conf.getVCDirectory();
		this.time = conf.getRecTime();

		this.history = 20;
		this.threshold = 11;
		this.rectangles = 2;
		this.size = 40;
	}

/**
 * Rejestruje obserwatora zdarzenia przekroczenia limitu osób
 * @param obs Obserwator implementujący interfejs VisitorsLimitExceeded
 * @see VisitorsLimitExceeded
 */
	public boolean addObserver(VisitorsLimitExceeded obs) {
		return observers.add(obs);
	}

/**
 * Usuwa obserwatora zdarzenia przekroczenia limitu osób
 * @param obs Obserwator implementujący interfejs VisitorsLimitExceeded
 * @see VisitorsLimitExceeded
 */
	public boolean removeObserver(VisitorsLimitExceeded obs) {
		while(observers.remove(obs)){}
		return true;
	}

/**
 * Uruchamia subproces liczący osoby w pomieszczeniu, przekazując w wywołaniu parametry z pliku konfiguracyjnego
 * @returns Instacja klasy Process uruchomionego programu
 */
	private Process runProcess() {
		String[] cmd = {"/bin/sh", "-c", this.directory + "/visitorcount -p "
											+ " -d " + this.device
											+ " -r " + this.rectangles
											+ " -f " + this.filename
											+ " -h " + this.history
											+ " -t " + this.threshold
											+ " -s " + this.size
				};

		Process p = null;

		ProcessBuilder builder = new ProcessBuilder(cmd);
		builder.directory(new File(this.directory));

		try {
			p = builder.start();
		} catch(IOException err) {
			err.printStackTrace();
		}

		return p;
	}

/**
 * Zabija subproces visitorcount
 */
	private void destroyProcess() {
		String[] cmd = {"/bin/sh", "-c", "killall visitorcount"};
		ProcessBuilder pb = new ProcessBuilder(cmd);
		pb.environment().put("HOME", "/root");
		try {
			pb.start();
		} catch (IOException e) {
			e.printStackTrace();
		}
	}

/**
 * Metoda wykonuje kilka zdjęć, następnie na ich podstawie określa prawdopodobną liczbę osób znajdujących się w kadrze.
 * @returns liczba odnalezionych osób
 */
	public int singleQuery() {
		synchronized(lock) {
			final Process p = runProcess();

			BufferedReader stdInput = new BufferedReader(new InputStreamReader(p.getInputStream()));

			int visitors = -1;

			try {
				String answer = stdInput.readLine();
				if(answer != null) {
					int out = Integer.parseInt(answer);
					if(out > -1)
						visitors = out;

					if(out == -2) {
						System.out.println("Sending message: system under attack");
						Phone.sendAnnihilationMessage();
					}
				}
			} catch(IOException err) {
				err.printStackTrace();
			}

			destroyProcess();

			System.out.println("Visitors: " + visitors);

			return visitors;
		}
	}

/**
 * Wątek monitorujący pomieszczenie przez określony czas. Działa w trybie asynchronicznym.
 * Wątek zostaje uruchomiony po wywołaniu metody watch()
 */
	private Thread thread = new Thread(new Runnable() {
		@Override
		public void run() {

			synchronized(lock) {
				final Process p = runProcess();

				BufferedReader stdError = new BufferedReader(new InputStreamReader(p.getErrorStream()));
				BufferedReader stdInput = new BufferedReader(new InputStreamReader(p.getInputStream()));

				Timer t = new Timer();
				t.schedule(new TimerTask() {
						public void run() {
							stop();
						}
					}, new Date(new Date().getTime() + time));

				int visitors = -1;

				try {
					String answer = null;
					while((answer = stdInput.readLine()) != null) {
						if(answer != null) {
							int out = Integer.parseInt(answer);
							if(out > -1) {
								visitors = out;

								if(visitors > visitorsLimit) {
									for(VisitorsLimitExceeded obs : observers) {
										System.out.println("Visitors: " + visitors);
										obs.notify(visitors);
									}
									destroyProcess();
								}
							}

							if(out == -2) {
								System.out.println("Sending message: system under attack");
								Phone.sendAnnihilationMessage();
							}
						}
					}
				} catch(IOException err) {
					err.printStackTrace();
				}

				try {
					String err = null;
					while((err = stdError.readLine()) != null) {
						System.err.println(err);
					}
				} catch(IOException err) {
					err.printStackTrace();
				}

				run = false;
				t.cancel();
				p.destroy();
			}
		}
	});

/**
 * Metoda uruchamia odrębny wątek, który obserwuje pomieszczenie przez określony czas.
 * Gdy liczba osób znajdujących się na zdjęciu przekracza dozwolony próg,
 * zarejestrowane obiekty są o tym informowane.
 * @param visitorsLimit maksymalna liczba osób jaka może być w pomieszczeniu
 * @returns wartość logiczna, określająca czy udało się uruchomić wątek
 */
	public boolean watch(int visitorsLimit) {
		if(!run) {
			this.visitorsLimit = visitorsLimit;
			run = true;
			thread.start();
			return true;
		}
		return false;
	}
/**
 * Metoda zatrzymuje wątek monitorujący liczbę osób w pomieszczeniu
 */
	public boolean stop() {
		if(run) {
			run = false;
			try {
				destroyProcess();
				thread.join();
			} catch(InterruptedException e) {
				e.printStackTrace();
			}
			return true;
		}
		return false;
	}
/**
 * Metoda dokonuje sprawdzenia, czy obraz jest nagrywany/wysyłany
 */
	public boolean isRunning() {
		return run;
	}
}
